﻿HmJS.$Import('core.fx');
HmJS.$Import('core.request');
//HmJS.$Import('more.spinner');

HmJS.register('core.form', function ($ns) {

	var document = window.document;

	// #region -- Element.Forms --

	/**
	* name: Element.Forms
	* 
	* description: Extends the Element native object to include methods useful in managing inputs.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Element
	*		- /String.Extras
	*		- /MooTools.More
	* 
	* provides: [Element.Forms]
	**/

	Element.implement({

		tidy: function () {
			this.set('value', this.get('value').tidy());
		},

		getTextInRange: function (start, end) {
			return this.get('value').substring(start, end);
		},

		getSelectedText: function () {
			if (this.setSelectionRange) {
				return this.getTextInRange(this.getSelectionStart(), this.getSelectionEnd());
			}
			return document.selection.createRange().text;
		},

		getSelectedRange: function () {
			if (this.selectionStart != null) {
				return {
					start: this.selectionStart,
					end: this.selectionEnd
				};
			}

			var pos = {
				start: 0,
				end: 0
			};
			var range = this.getDocument().selection.createRange();
			if (!range || range.parentElement() != this) {
				return pos;
			}
			var duplicate = range.duplicate();

			if (this.type == 'text') {
				pos.start = 0 - duplicate.moveStart('character', -100000);
				pos.end = pos.start + range.text.length;
			} else {
				var value = this.get('value');
				var offset = value.length;
				duplicate.moveToElementText(this);
				duplicate.setEndPoint('StartToEnd', range);
				if (duplicate.text.length) {
					offset -= value.match(/[\n\r]*$/)[0].length;
				}
				pos.end = offset - duplicate.text.length;
				duplicate.setEndPoint('StartToStart', range);
				pos.start = offset - duplicate.text.length;
			}
			return pos;
		},

		getSelectionStart: function () {
			return this.getSelectedRange().start;
		},

		getSelectionEnd: function () {
			return this.getSelectedRange().end;
		},

		setCaretPosition: function (pos) {
			if (pos == 'end') {
				pos = this.get('value').length;
			}
			this.selectRange(pos, pos);
			return this;
		},

		getCaretPosition: function () {
			return this.getSelectedRange().start;
		},

		selectRange: function (start, end) {
			if (this.setSelectionRange) {
				this.focus();
				this.setSelectionRange(start, end);
			} else {
				var value = this.get('value');
				var diff = value.substr(start, end - start).replace(/\r/g, '').length;
				start = value.substr(0, start).replace(/\r/g, '').length;
				var range = this.createTextRange();
				range.collapse(true);
				range.moveEnd('character', start + diff);
				range.moveStart('character', start);
				range.select();
			}
			return this;
		},

		insertAtCursor: function (value, select) {
			var pos = this.getSelectedRange();
			var text = this.get('value');
			this.set('value', text.substring(0, pos.start) + value + text.substring(pos.end, text.length));
			if (select !== false) {
				this.selectRange(pos.start, pos.start + value.length);
			}
			else this.setCaretPosition(pos.start + value.length);
			return this;
		},

		insertAroundCursor: function (options, select) {
			options = Object.append({
				before: '',
				defaultMiddle: '',
				after: ''
			}, options);

			var value = this.getSelectedText() || options.defaultMiddle;
			var pos = this.getSelectedRange();
			var text = this.get('value');

			if (pos.start == pos.end) {
				this.set('value', text.substring(0, pos.start) + options.before + value + options.after + text.substring(pos.end, text.length));
				this.selectRange(pos.start + options.before.length, pos.end + options.before.length + value.length);
			} else {
				var current = text.substring(pos.start, pos.end);
				this.set('value', text.substring(0, pos.start) + options.before + current + options.after + text.substring(pos.end, text.length));
				var selStart = pos.start + options.before.length;
				if (select !== false) {
					this.selectRange(selStart, selStart + current.length);
				}
				else this.setCaretPosition(selStart + text.length);
			}
			return this;
		}

	});

	// #endregion

	// #region -- Elements.From --

	/**
	* name: Elements.From
	* 
	* description: Returns a collection of elements from a string of html.
	* 
	* authors:
	* - Aaron Newton
	* 
	* requires:
	* - Core/String
	* - Core/Element
	* - /MooTools.More
	* 
	* provides: [Elements.from, Elements.From]
	**/

	Elements.from = function (text, excludeScripts) {
		if (excludeScripts || excludeScripts == null) {
			text = text.stripScripts();
		}

		var container, match = text.match(/^\s*<(t[dhr]|tbody|tfoot|thead)/i);

		if (match) {
			container = new Element('table');
			var tag = match[1].toLowerCase();
			if (['td', 'th', 'tr'].contains(tag)) {
				container = new Element('tbody').inject(container);
				if (tag != 'tr') {
					container = new Element('tr').inject(container);
				}
			}
		}

		return (container || new Element('div')).set('html', text).getChildren();
	};

	// #endregion

	// #region -- Form.Request --

	/**
	* name: Form.Request
	* 
	* description: Handles the basic functionality of submitting a form and updating a dom element with the result.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Request.HTML
	*		- /Class.Binds
	*		- /Class.Occlude
	*		- /Spinner
	*		- /String.QueryString
	*		- /Element.Delegation
	* 
	* provides: [Form.Request]
	**/

	if (!window.Form) { window.Form = {}; }

	(function () {

		Form.Request = new Class({

			Binds: ['onSubmit', 'onFormValidate'],

			Implements: [Options, Events, Class.Occlude],

			options: {/*
			onFailure: function(){},
			onSuccess: function(){}, // aliased to onComplete,
			onSend: function(){}*/
				requestOptions: {
					evalScripts: true,
					useSpinner: true,
					emulation: false,
					link: 'ignore'
				},
				sendButtonClicked: true,
				extraData: {},
				resetForm: true
			},

			property: 'form.request',

			initialize: function (form, target, options) {
				this.element = document.id(form);
				if (this.occlude()) { return this.occluded; }
				this.setOptions(options)
					.setTarget(target)
					.attach();
			},

			setTarget: function (target) {
				this.target = document.id(target);
				if (!this.request) {
					this.makeRequest();
				} else {
					this.request.setOptions({
						update: this.target
					});
				}
				return this;
			},

			toElement: function () {
				return this.element;
			},

			makeRequest: function () {
				var self = this;
				this.request = new Request.HTML(Object.merge({
					update: this.target,
					emulation: false,
					spinnerTarget: this.element,
					method: this.element.get('method') || 'post'
				}, this.options.requestOptions)).addEvents({
					success: function (tree, elements, html, javascript) {
						['complete', 'success'].each(function (evt) {
							self.fireEvent(evt, [self.target, tree, elements, html, javascript]);
						});
					},
					failure: function () {
						self.fireEvent('complete', arguments).fireEvent('failure', arguments);
					},
					exception: function () {
						self.fireEvent('failure', arguments);
					}
				});
				return this.attachReset();
			},

			attachReset: function () {
				if (!this.options.resetForm) { return this; }
				this.request.addEvent('success', function () {
					Function.attempt(function () {
						this.element.reset();
					} .bind(this));
					if (window.OverText) { OverText.update(); }
				} .bind(this));
				return this;
			},

			attach: function (attach) {
				var method = (attach != false) ? 'addEvent' : 'removeEvent';
				this.element[method]('click:relay(button, input[type=submit])', this.saveClickedButton.bind(this));

				var fv = this.element.retrieve('validator');
				if (fv) {
					fv[method]('onFormValidate', this.onFormValidate);
				} else {
					this.element[method]('submit', this.onSubmit);
				}

				return this;
			},

			detach: function () {
				return this.attach(false);
			},

			//public method
			enable: function () {
				return this.attach();
			},

			//public method
			disable: function () {
				return this.detach();
			},

			onFormValidate: function (valid, form, event) {
				//if there's no event, then this wasn't a submit event
				if (!event) { return; }
				var fv = this.element.retrieve('validator');
				if (valid || (fv && !fv.options.stopOnFailure)) {
					event.stop();
					this.send();
				}
			},

			onSubmit: function (event) {
				var fv = this.element.retrieve('validator');
				if (fv) {
					//form validator was created after Form.Request
					this.element.removeEvent('submit', this.onSubmit);
					fv.addEvent('onFormValidate', this.onFormValidate);
					this.element.validate();
					return;
				}
				if (event) { event.stop(); }
				this.send();
			},

			saveClickedButton: function (event, target) {
				var targetName = target.get('name');
				if (!targetName || !this.options.sendButtonClicked) {
					return;
				}
				this.options.extraData[targetName] = target.get('value') || true;
				this.clickedCleaner = function () {
					delete this.options.extraData[targetName];
					this.clickedCleaner = function () { };
				} .bind(this);
			},

			clickedCleaner: function () { },

			send: function () {
				var str = this.element.toQueryString().trim(),
				data = Object.toQueryString(this.options.extraData);

				if (str) {
					str += "&" + data;
				} else {
					str = data;
				}

				this.fireEvent('send', [this.element, str.parseQueryString()]);
				this.request.send({
					data: str,
					url: this.options.requestOptions.url || this.element.get('action')
				});
				this.clickedCleaner();
				return this;
			}

		});

		Element.implement('formUpdate', function (update, options) {
			var fq = this.retrieve('form.request');
			if (!fq) {
				fq = new Form.Request(this, update, options);
			} else {
				if (update) {
					fq.setTarget(update);
				}
				if (options) {
					fq.setOptions(options).makeRequest();
				}
			}
			fq.send();
			return this;
		});

	})();

	// #endregion

	// #region -- Form.Request.Append --

	/**
	* name: Form.Request.Append
	* 
	* description: Handles the basic functionality of submitting a form and updating a dom element with the result. The result is appended to the DOM element instead of replacing its contents.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- /Form.Request
	*		- /Fx.Reveal
	*		- /Elements.from
	* 
	* provides: [Form.Request.Append]
	**/

	Form.Request.Append = new Class({

		Extends: Form.Request,

		options: {
			//onBeforeEffect: function(){},
			useReveal: true,
			revealOptions: {},
			inject: 'bottom'
		},

		makeRequest: function () {
			this.request = new Request.HTML(Object.merge({
				url: this.element.get('action'),
				method: this.element.get('method') || 'post',
				spinnerTarget: this.element
			}, this.options.requestOptions, {
				evalScripts: false
			})
		).addEvents({
			success: function (tree, elements, html, javascript) {
				var container;
				var kids = Elements.from(html);
				if (kids.length == 1) {
					container = kids[0];
				} else {
					container = new Element('div', {
						styles: {
							display: 'none'
						}
					}).adopt(kids);
				}
				container.inject(this.target, this.options.inject);
				if (this.options.requestOptions.evalScripts) {
					Browser.exec(javascript);
				}
				this.fireEvent('beforeEffect', container);
				var finish = function () {
					this.fireEvent('success', [container, this.target, tree, elements, html, javascript]);
				} .bind(this);
				if (this.options.useReveal) {
					container.set('reveal', this.options.revealOptions).get('reveal').chain(finish);
					container.reveal();
				} else {
					finish();
				}
			} .bind(this),
			failure: function (xhr) {
				this.fireEvent('failure', xhr);
			} .bind(this)
		});
			this.attachReset();
		}

	});

	// #endregion

	// #region -- Locale.en-US.Form.Validator --

	/**
	* name: Locale.en-US.Form.Validator
	* 
	* description: Form Validator messages for English.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- /Locale
	* 
	* provides: [Locale.en-US.Form.Validator]
	**/

	Locale.define('en-US', 'FormValidator', {

		required: 'This field is required.',
		length: 'Please enter {length} characters (you entered {elLength} characters)',
		minLength: 'Please enter at least {minLength} characters (you entered {length} characters).',
		maxLength: 'Please enter no more than {maxLength} characters (you entered {length} characters).',
		integer: 'Please enter an integer in this field. Numbers with decimals (e.g. 1.25) are not permitted.',
		numeric: 'Please enter only numeric values in this field (i.e. "1" or "1.1" or "-1" or "-1.1").',
		digits: 'Please use numbers and punctuation only in this field (for example, a phone number with dashes or dots is permitted).',
		alpha: 'Please use only letters (a-z) within this field. No spaces or other characters are allowed.',
		alphanum: 'Please use only letters (a-z) or numbers (0-9) in this field. No spaces or other characters are allowed.',
		dateSuchAs: 'Please enter a valid date such as {date}',
		dateInFormatMDY: 'Please enter a valid date such as MM/DD/YYYY (i.e. "12/31/1999")',
		email: 'Please enter a valid email address. For example "fred@domain.com".',
		url: 'Please enter a valid URL such as http://www.example.com.',
		currencyDollar: 'Please enter a valid $ amount. For example $100.00 .',
		oneRequired: 'Please enter something for at least one of these inputs.',
		errorPrefix: 'Error: ',
		warningPrefix: 'Warning: ',

		// Form.Validator.Extras
		noSpace: 'There can be no spaces in this input.',
		reqChkByNode: 'No items are selected.',
		requiredChk: 'This field is required.',
		reqChkByName: 'Please select a {label}.',
		match: 'This field needs to match the {matchName} field',
		startDate: 'the start date',
		endDate: 'the end date',
		currendDate: 'the current date',
		afterDate: 'The date should be the same or after {label}.',
		beforeDate: 'The date should be the same or before {label}.',
		startMonth: 'Please select a start month',
		sameMonth: 'These two dates must be in the same month - you must change one or the other.',
		creditcard: 'The credit card number entered is invalid. Please check the number and try again. {length} digits entered.'

	});

	// #endregion Locale.en-US.Form.Validator

	// #region -- Locale.zh-CH.Form.Validator --

	/**
	* name: Locale.zh-CH.Form.Validator
	* 
	* description: Form Validator messages for Chinese (simplified and traditional).
	* 
	* authors:
	*		- YMind Chan
	* 
	* requires:
	*		- /Locale
	*		- /Form.Validator
	* 
	* provides: [Form.zh-CH.Form.Validator, Form.Validator.CurrencyYuanValidator]
	**/

	// Simplified Chinese
	Locale.define('zh-CHS', 'FormValidator', {

		required: '此项必填。',
		length: 'Please enter {length} characters (you entered {elLength} characters)',
		minLength: '请至少输入 {minLength} 个字符 (已输入 {length} 个)。',
		maxLength: '最多只能输入 {maxLength} 个字符 (已输入 {length} 个)。',
		integer: '请输入一个整数，不能包含小数点。例如："1", "200"。',
		numeric: '请输入一个数字，例如："1", "1.1", "-1", "-1.1"。',
		digits: '请输入由数字和标点符号组成的内容。例如电话号码。',
		alpha: '请输入 A-Z 的 26 个字母，不能包含空格或任何其他字符。',
		alphanum: '请输入 A-Z 的 26 个字母或 0-9 的 10 个数字，不能包含空格或任何其他字符。',
		dateSuchAs: '请输入合法的日期格式，如：{date}。',
		dateInFormatMDY: '请输入合法的日期格式，例如：YYYY-MM-DD ("2010-12-31")。',
		email: '请输入合法的电子信箱地址，例如："fred@domain.com"。',
		url: '请输入合法的 Url 地址，例如：http://www.example.com。',
		currencyDollar: '请输入合法的货币符号，例如：￥100.0',
		oneRequired: '请至少选择一项。',
		errorPrefix: '错误：',
		warningPrefix: '警告：',

		// Form.Validator.Extras
		noSpace: '不能包含空格。',
		reqChkByNode: '未选择任何内容。',
		requiredChk: '此项必填。',
		reqChkByName: '请选择 {label}.',
		match: '必须与{matchName}相匹配',
		startDate: '起始日期',
		endDate: '结束日期',
		currendDate: '当前日期',
		afterDate: '日期必须等于或晚于 {label}.',
		beforeDate: '日期必须早于或等于 {label}.',
		startMonth: '请选择起始月份',
		sameMonth: '您必须修改两个日期中的一个，以确保它们在同一月份。',
		creditcard: '您输入的信用卡号码不正确。当前已输入{length}个字符。'

	});

	// Traditional Chinese
	Locale.define('zh-CHT', 'FormValidator', {

		required: '此項必填。 ',
		length: 'Please enter {length} characters (you entered {elLength} characters)',
		minLength: '請至少輸入{minLength} 個字符(已輸入{length} 個)。 ',
		maxLength: '最多只能輸入{maxLength} 個字符(已輸入{length} 個)。 ',
		integer: '請輸入一個整數，不能包含小數點。例如："1", "200"。 ',
		numeric: '請輸入一個數字，例如："1", "1.1", "-1", "-1.1"。 ',
		digits: '請輸入由數字和標點符號組成的內容。例如電話號碼。 ',
		alpha: '請輸入AZ 的26 個字母，不能包含空格或任何其他字符。 ',
		alphanum: '請輸入AZ 的26 個字母或0-9 的10 個數字，不能包含空格或任何其他字符。 ',
		dateSuchAs: '請輸入合法的日期格式，如：{date}。 ',
		dateInFormatMDY: '請輸入合法的日期格式，例如：YYYY-MM-DD ("2010-12-31")。 ',
		email: '請輸入合法的電子信箱地址，例如："fred@domain.com"。 ',
		url: '請輸入合法的Url 地址，例如：http://www.example.com。 ',
		currencyDollar: '請輸入合法的貨幣符號，例如：￥100.0',
		oneRequired: '請至少選擇一項。 ',
		errorPrefix: '錯誤：',
		warningPrefix: '警告：',

		// Form.Validator.Extras
		noSpace: '不能包含空格。 ',
		reqChkByNode: '未選擇任何內容。 ',
		requiredChk: '此項必填。 ',
		reqChkByName: '請選擇 {label}.',
		match: '必須與{matchName}相匹配',
		startDate: '起始日期',
		endDate: '結束日期',
		currendDate: '當前日期',
		afterDate: '日期必須等於或晚於{label}.',
		beforeDate: '日期必須早於或等於{label}.',
		startMonth: '請選擇起始月份',
		sameMonth: '您必須修改兩個日期中的一個，以確保它們在同一月份。 ',
		creditcard: '您輸入的信用卡號碼不正確。當前已輸入{length}個字符。 '

	});

	// #endregion Form Locale.zh-CH.Form.Validator

	// #region -- Form.Validator Class --

	/**
	* name: Form.Validator
	* 
	* description: A css-class based form validation system.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Options
	*		- Core/Events
	*		- Core/Slick.Finder
	*		- Core/Element.Event
	*		- Core/Element.Style
	*		- Core/JSON
	*		- /Locale
	*		- /Class.Binds
	*		- /Date
	*		- /Element.Forms
	*		- /Locale.en-US.Form.Validator
	*		- /Element.Shortcuts
	* 
	* provides: [Form.Validator, InputValidator, FormValidator.BaseValidators]
	**/

	if (!window.Form) { window.Form = {}; }

	var InputValidator = this.InputValidator = new Class({

		Implements: [Options],

		options: {
			errorMsg: 'Validation failed.',
			test: Function.from(true)
		},

		initialize: function (className, options) {
			this.setOptions(options);
			this.className = className;
		},

		test: function (field, props) {
			field = document.id(field);
			return (field) ? this.options.test(field, props || this.getProps(field)) : false;
		},

		getError: function (field, props) {
			field = document.id(field);
			var err = this.options.errorMsg;
			if (typeOf(err) == 'function') {
				err = err(field, props || this.getProps(field));
			}
			return err;
		},

		getProps: function (field) {
			field = document.id(field);
			return (field) ? field.get('validatorProps') : {};
		}

	});

	Element.Properties.validators = {

		get: function () {
			return (this.get('data-validators') || this.className).clean().split(' ');
		}

	};

	Element.Properties.validatorProps = {

		set: function (props) {
			return this.eliminate('$moo:validatorProps').store('$moo:validatorProps', props);
		},

		get: function (props) {
			if (props) {
				this.set(props);
			}
			if (this.retrieve('$moo:validatorProps')) {
				return this.retrieve('$moo:validatorProps');
			}
			if (this.getProperty('data-validator-properties') || this.getProperty('validatorProps')) {
				try {
					this.store('$moo:validatorProps', JSON.decode(this.getProperty('validatorProps') || this.getProperty('data-validator-properties')));
				} catch (e) {
					return {};
				}
			} else {
				var vals = this.get('validators').filter(function (cls) {
					return cls.test(':');
				});
				if (!vals.length) {
					this.store('$moo:validatorProps', {});
				} else {
					props = {};
					vals.each(function (cls) {
						var split = cls.split(':');
						if (split[1]) {
							try {
								props[split[0]] = JSON.decode(split[1]);
							} catch (e) { }
						}
					});
					this.store('$moo:validatorProps', props);
				}
			}
			return this.retrieve('$moo:validatorProps');
		}

	};

	Form.Validator = new Class({

		Implements: [Options, Events],

		Binds: ['onSubmit'],

		options: {/*
		onFormValidate: function(isValid, form, event){},
		onElementValidate: function(isValid, field, className, warn){},
		onElementPass: function(field){},
		onElementFail: function(field, validatorsFailed){}, */
			fieldSelectors: 'input, select, textarea',
			ignoreHidden: true,
			ignoreDisabled: true,
			useTitles: false,
			evaluateOnSubmit: true,
			evaluateFieldsOnBlur: true,
			evaluateFieldsOnChange: true,
			serial: true,
			stopOnFailure: true,
			warningPrefix: function () {
				return Form.Validator.getMsg('warningPrefix') || 'Warning: ';
			},
			errorPrefix: function () {
				return Form.Validator.getMsg('errorPrefix') || 'Error: ';
			}
		},

		initialize: function (form, options) {
			this.setOptions(options);
			this.element = document.id(form);
			this.element.store('validator', this);
			this.warningPrefix = Function.from(this.options.warningPrefix)();
			this.errorPrefix = Function.from(this.options.errorPrefix)();
			if (this.options.evaluateOnSubmit) {
				this.element.addEvent('submit', this.onSubmit);
			}
			if (this.options.evaluateFieldsOnBlur || this.options.evaluateFieldsOnChange) {
				this.watchFields(this.getFields());
			}
		},

		toElement: function () {
			return this.element;
		},

		getFields: function () {
			return (this.fields = this.element.getElements(this.options.fieldSelectors));
		},

		watchFields: function (fields) {
			fields.each(function (el) {
				if (this.options.evaluateFieldsOnBlur) {
					el.addEvent('blur', this.validationMonitor.pass([el, false], this));
				}
				if (this.options.evaluateFieldsOnChange) {
					el.addEvent('change', this.validationMonitor.pass([el, true], this));
				}
			}, this);
		},

		validationMonitor: function () {
			clearTimeout(this.timer);
			this.timer = this.validateField.delay(50, this, arguments);
		},

		onSubmit: function (event) {
			if (this.validate(event)) { this.reset(); }
		},

		reset: function () {
			this.getFields().each(this.resetField, this);
			return this;
		},

		validate: function (event) {
			var result = this.getFields().map(function (field) {
				return this.validateField(field, true);
			}, this).every(function (v) {
				return v;
			});
			this.fireEvent('formValidate', [result, this.element, event]);
			if (this.options.stopOnFailure && !result && event) {
				event.preventDefault();
			}
			return result;
		},

		validateField: function (field, force) {
			if (this.paused) { return true; }
			field = document.id(field);
			var passed = !field.hasClass('validation-failed');
			var failed, warned;
			if (this.options.serial && !force) {
				failed = this.element.getElement('.validation-failed');
				warned = this.element.getElement('.warning');
			}
			if (field && (!failed || force || field.hasClass('validation-failed') || (failed && !this.options.serial))) {
				var validationTypes = field.get('validators');
				var validators = validationTypes.some(function (cn) {
					return this.getValidator(cn);
				}, this);
				var validatorsFailed = [];
				validationTypes.each(function (className) {
					if (className && !this.test(className, field)) {
						validatorsFailed.include(className);
					}
				}, this);
				passed = validatorsFailed.length === 0;
				if (validators && !this.hasValidator(field, 'warnOnly')) {
					if (passed) {
						field.addClass('validation-passed').removeClass('validation-failed');
						this.fireEvent('elementPass', [field]);
					} else {
						field.addClass('validation-failed').removeClass('validation-passed');
						this.fireEvent('elementFail', [field, validatorsFailed]);
					}
				}
				if (!warned) {
					var warnings = validationTypes.some(function (cn) {
						if (cn.test('^warn')) {
							return this.getValidator(cn.replace(/^warn-/, ''));
						} else {
							return null;
						}
					}, this);
					field.removeClass('warning');
					var warnResult = validationTypes.map(function (cn) {
						if (cn.test('^warn')) {
							return this.test(cn.replace(/^warn-/, ''), field, true);
						} else {
							return null;
						}
					}, this);
				}
			}
			return passed;
		},

		test: function (className, field, warn) {
			field = document.id(field);
			if ((this.options.ignoreHidden && !field.isVisible()) || (this.options.ignoreDisabled && field.get('disabled'))) {
				return true;
			}
			var validator = this.getValidator(className);
			if (warn != null) {
				warn = false;
			}
			if (this.hasValidator(field, 'warnOnly')) {
				warn = true;
			}
			var isValid = this.hasValidator(field, 'ignoreValidation') || (validator ? validator.test(field) : true);
			if (validator && field.isVisible()) {
				this.fireEvent('elementValidate', [isValid, field, className, warn]);
			}
			if (warn) { return true; }
			return isValid;
		},

		hasValidator: function (field, value) {
			return field.get('validators').contains(value);
		},

		resetField: function (field) {
			field = document.id(field);
			if (field) {
				field.get('validators').each(function (className) {
					if (className.test('^warn-')) {
						className = className.replace(/^warn-/, '');
					}
					field.removeClass('validation-failed');
					field.removeClass('warning');
					field.removeClass('validation-passed');
				}, this);
			}
			return this;
		},

		stop: function () {
			this.paused = true;
			return this;
		},

		start: function () {
			this.paused = false;
			return this;
		},

		ignoreField: function (field, warn) {
			field = document.id(field);
			if (field) {
				this.enforceField(field);
				if (warn) {
					field.addClass('warnOnly');
				} else {
					field.addClass('ignoreValidation');
				}
			}
			return this;
		},

		enforceField: function (field) {
			field = document.id(field);
			if (field) {
				field.removeClass('warnOnly').removeClass('ignoreValidation');
			}
			return this;
		}

	});

	Form.Validator.getMsg = function (key) {
		return Locale.get('FormValidator.' + key);
	};

	Form.Validator.adders = {

		validators: {},

		add: function (className, options) {
			this.validators[className] = new InputValidator(className, options);
			//if this is a class (this method is used by instances of Form.Validator and the Form.Validator namespace)
			//extend these validators into it
			//this allows validators to be global and/or per instance
			if (!this.initialize) {
				this.implement({
					validators: this.validators
				});
			}
		},

		addAllThese: function (validators) {
			Array.from(validators).each(function (validator) {
				this.add(validator[0], validator[1]);
			}, this);
		},

		getValidator: function (className) {
			return this.validators[className.split(':')[0]];
		}

	};

	Object.append(Form.Validator, Form.Validator.adders);

	Form.Validator.implement(Form.Validator.adders);

	Form.Validator.add('IsEmpty', {

		errorMsg: false,
		test: function (element) {
			if (element.type == 'select-one' || element.type == 'select') {
				return !(element.selectedIndex >= 0 && element.options[element.selectedIndex].value != '');
			} else {
				return ((element.get('value') == null) || (element.get('value').length == 0));
			}
		}

	});

	Form.Validator.addAllThese([

	['required', {
		errorMsg: function () {
			return Form.Validator.getMsg('required');
		},
		test: function (element) {
			return !Form.Validator.getValidator('IsEmpty').test(element);
		}
	}],

	['length', {
		errorMsg: function (element, props) {
			if (typeOf(props.length) != 'null')
				return Form.Validator.getMsg('length').substitute({ length: props.length, elLength: element.get('value').length });
			else return '';
		},
		test: function (element, props) {
			if (typeOf(props.length) != 'null') return (element.get('value').length == props.length || element.get('value').length == 0);
			else return true;
		}
	}],

	['minLength', {
		errorMsg: function (element, props) {
			if (typeOf(props.minLength) != 'null') {
				return Form.Validator.getMsg('minLength').substitute({ minLength: props.minLength, length: element.get('value').length });
			} else {
				return '';
			}
		},
		test: function (element, props) {
			if (typeOf(props.minLength) != 'null') {
				return (element.get('value').length >= (props.minLength || 0));
			} else {
				return true;
			}
		}
	}],

	['maxLength', {
		errorMsg: function (element, props) {
			//props is {maxLength:10}
			if (typeOf(props.maxLength) != 'null') {
				return Form.Validator.getMsg('maxLength').substitute({ maxLength: props.maxLength, length: element.get('value').length });
			} else {
				return '';
			}
		},
		test: function (element, props) {
			return element.get('value').length <= (props.maxLength || 10000);
		}
	}],

	['validate-integer', {
		errorMsg: Form.Validator.getMsg.pass('integer'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^(-?[1-9]\d*|0)$/).test(element.get('value'));
		}
	}],

	['validate-numeric', {
		errorMsg: Form.Validator.getMsg.pass('numeric'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) ||
				(/^-?(?:0$0(?=\d*\.)|[1-9]|0)\d*(\.\d+)?$/).test(element.get('value'));
		}
	}],

	['validate-digits', {
		errorMsg: Form.Validator.getMsg.pass('digits'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^[\d() .:\-\+#]+$/.test(element.get('value')));
		}
	}],

	['validate-alpha', {
		errorMsg: Form.Validator.getMsg.pass('alpha'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^[a-zA-Z]+$/).test(element.get('value'));
		}
	}],

	['validate-alphanum', {
		errorMsg: Form.Validator.getMsg.pass('alphanum'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || !(/\W/).test(element.get('value'));
		}
	}],

	['validate-date', {
		errorMsg: function (element, props) {
			if (Date.parse) {
				var format = props.dateFormat || '%x';
				return Form.Validator.getMsg('dateSuchAs').substitute({ date: new Date().format(format) });
			} else {
				return Form.Validator.getMsg('dateInFormatMDY');
			}
		},
		test: function (element, props) {
			if (Form.Validator.getValidator('IsEmpty').test(element)) {
				return true;
			}
			var dateLocale = Locale.getCurrent().sets.Date,
					dateNouns = new RegExp([dateLocale.days, dateLocale.days_abbr, dateLocale.months, dateLocale.months_abbr].flatten().join('|'), 'i'),
					value = element.get('value'),
					wordsInValue = value.match(/[a-z]+/gi);

			if (wordsInValue && !wordsInValue.every(dateNouns.exec, dateNouns)) {
				return false;
			}

			var date = Date.parse(value),
					format = props.dateFormat || '%x',
					formatted = date.format(format);

			if (formatted != 'invalid date') {
				element.set('value', formatted);
			}
			return date.isValid();
		}
	}],

	['validate-email', {
		errorMsg: Form.Validator.getMsg.pass('email'),
		test: function (element) {
			/*
			var chars = "[a-z0-9!#$%&'*+/=?^_`{|}~-]",
			local = '(?:' + chars + '\\.?){0,63}' + chars,

			label = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?',
			hostname = '(?:' + label + '\\.)*' + label;

			octet = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)',
			ipv4 = '\\[(?:' + octet + '\\.){3}' + octet + '\\]',

			domain = '(?:' + hostname + '|' + ipv4 + ')';

			var regex = new RegExp('^' + local + '@' + domain + '$', 'i');
			*/
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]\.?){0,63}[a-z0-9!#$%&'*+\/=?^_`{|}~-]@(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])$/i).test(element.get('value'));
		}
	}],

	['validate-url', {
		errorMsg: Form.Validator.getMsg.pass('url'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i).test(element.get('value'));
		}
	}],

	['validate-currency-dollar', {
		errorMsg: Form.Validator.getMsg.pass('currencyDollar'),
		test: function (element) {
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^\$?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/).test(element.get('value'));
		}
	}],

	['validate-one-required', {
		errorMsg: Form.Validator.getMsg.pass('oneRequired'),
		test: function (element, props) {
			var p = document.id(props['validate-one-required']) || element.getParent(props['validate-one-required']);
			return p.getElements('input').some(function (el) {
				if (['checkbox', 'radio'].contains(el.get('type'))) {
					return el.get('checked');
				}
				return el.get('value');
			});
		}
	}]

]);

	Element.Properties.validator = {

		set: function (options) {
			this.get('validator').setOptions(options);
		},

		get: function () {
			var validator = this.retrieve('validator');
			if (!validator) {
				validator = new Form.Validator(this);
				this.store('validator', validator);
			}
			return validator;
		}

	};

	Element.implement({

		validate: function (options) {
			if (options) {
				this.set('validator', options);
			}
			return this.get('validator').validate();
		}

	});

	// #endregion Form.Validator

	// #region -- Form.Validator.Inline Class --

	/**
	* name: Form.Validator.Inline
	* 
	* description: Extends Form.Validator to add inline messages.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- /Form.Validator
	* 
	* provides: [Form.Validator.Inline]
	**/

	Form.Validator.Inline = new Class({

		Extends: Form.Validator,

		options: {
			showError: function (errorElement) {
				if (errorElement.reveal) {
					errorElement.reveal();
				} else {
					errorElement.setStyle('display', 'block');
				}
			},
			hideError: function (errorElement) {
				if (errorElement.dissolve) {
					errorElement.dissolve();
				} else {
					errorElement.setStyle('display', 'none');
				}
			},
			scrollToErrorsOnSubmit: true,
			scrollToErrorsOnBlur: false,
			scrollToErrorsOnChange: false,
			scrollFxOptions: {
				transition: 'quad:out',
				offset: {
					y: -20
				}
			}
		},

		initialize: function (form, options) {
			this.parent(form, options);
			this.addEvent('onElementValidate', function (isValid, field, className, warn) {
				var validator = this.getValidator(className);
				if (!isValid && validator.getError(field)) {
					if (warn) { field.addClass('warning'); }
					var advice = this.makeAdvice(className, field, validator.getError(field), warn);
					this.insertAdvice(advice, field);
					this.showAdvice(className, field);
				} else {
					this.hideAdvice(className, field);
				}
			});
		},

		makeAdvice: function (className, field, error, warn) {
			var errorMsg = (warn) ? this.warningPrefix : this.errorPrefix;
			errorMsg += (this.options.useTitles) ? field.title || error : error;
			var cssClass = (warn) ? 'warning-advice' : 'validation-advice';
			var advice = this.getAdvice(className, field);
			if (advice) {
				advice = advice.set('html', errorMsg);
			} else {
				advice = new Element('div', {
					html: errorMsg,
					styles: { display: 'none' },
					id: 'advice-' + className.split(':')[0] + '-' + this.getFieldId(field)
				}).addClass(cssClass);
			}
			field.store('$moo:advice-' + className, advice);
			return advice;
		},

		getFieldId: function (field) {
			return field.id ? field.id : field.id = 'input_' + field.name;
		},

		showAdvice: function (className, field) {
			var advice = this.getAdvice(className, field);
			if (advice &&
					!field.retrieve('$moo:' + this.getPropName(className)) &&
					(advice.getStyle('display') == 'none' ||
					advice.getStyle('visiblity') == 'hidden' ||
					advice.getStyle('opacity') == 0)) {
				field.store('$moo:' + this.getPropName(className), true);
				this.options.showError(advice);
				this.fireEvent('showAdvice', [field, advice, className]);
			}
		},

		hideAdvice: function (className, field) {
			var advice = this.getAdvice(className, field);
			if (advice && field.retrieve('$moo:' + this.getPropName(className))) {
				field.store('$moo:' + this.getPropName(className), false);
				this.options.hideError(advice);
				this.fireEvent('hideAdvice', [field, advice, className]);
			}
		},

		getPropName: function (className) {
			return 'advice' + className;
		},

		resetField: function (field) {
			field = document.id(field);
			if (!field) { return this; }
			this.parent(field);
			field.get('validators').each(function (className) {
				this.hideAdvice(className, field);
			}, this);
			return this;
		},

		getAllAdviceMessages: function (field, force) {
			var advice = [];
			if (field.hasClass('ignoreValidation') && !force) {
				return advice;
			}
			var validators = field.get('validators').some(function (cn) {
				var warner = cn.test('^warn-') || field.hasClass('warnOnly');
				if (warner) {
					cn = cn.replace(/^warn-/, '');
				}
				var validator = this.getValidator(cn);
				if (!validator) { return; }
				advice.push({
					message: validator.getError(field),
					warnOnly: warner,
					passed: validator.test(),
					validator: validator
				});
			}, this);
			return advice;
		},

		getAdvice: function (className, field) {
			return field.retrieve('$moo:advice-' + className);
		},

		insertAdvice: function (advice, field) {
			//Check for error position prop
			var props = field.get('validatorProps');
			//Build advice
			if (!props.msgPos || !document.id(props.msgPos)) {
				if (field.type && field.type.toLowerCase() == 'radio') {
					field.getParent().adopt(advice);
				} else {
					advice.inject(document.id(field), 'after');
				}
			} else {
				document.id(props.msgPos).grab(advice);
			}
		},

		validateField: function (field, force, scroll) {
			var result = this.parent(field, force);
			if (((this.options.scrollToErrorsOnSubmit && scroll == null) || scroll) && !result) {
				var failed = document.id(this).getElement('.validation-failed');
				var par = document.id(this).getParent();
				while (par != document.body && par.getScrollSize().y == par.getSize().y) {
					par = par.getParent();
				}
				var fx = par.retrieve('$moo:fvScroller');
				if (!fx && window.Fx && Fx.Scroll) {
					fx = new Fx.Scroll(par, this.options.scrollFxOptions);
					par.store('$moo:fvScroller', fx);
				}
				if (failed) {
					if (fx) {
						fx.toElement(failed);
					} else {
						par.scrollTo(par.getScroll().x, failed.getPosition(par).y - 20);
					}
				}
			}
			return result;
		},

		watchFields: function (fields) {
			fields.each(function (el) {
				if (this.options.evaluateFieldsOnBlur) {
					el.addEvent('blur', this.validationMonitor.pass([el, false, this.options.scrollToErrorsOnBlur], this));
				}
				if (this.options.evaluateFieldsOnChange) {
					el.addEvent('change', this.validationMonitor.pass([el, true, this.options.scrollToErrorsOnChange], this));
				}
			}, this);
		}

	});

	// #endregion Form.Validator.Inline

	// #region -- Form.Validator.Extras --

	/**
	* name: Form.Validator.Extras
	* 
	* description: Additional validators for the Form.Validator class.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- /Form.Validator
	* 
	* provides: [Form.Validator.Extras]
	**/

	Form.Validator.addAllThese([

	['validate-enforce-oncheck', {
		test: function (element, props) {
			var fv = element.getParent('form').retrieve('validator');
			if (!fv) return true;
			(props.toEnforce || document.id(props.enforceChildrenOf).getElements('input, select, textarea')).map(function (item) {
				if (element.checked) {
					fv.enforceField(item);
				} else {
					fv.ignoreField(item);
					fv.resetField(item);
				}
			});
			return true;
		}
	}],

	['validate-ignore-oncheck', {
		test: function (element, props) {
			var fv = element.getParent('form').retrieve('validator');
			if (!fv) return true;
			(props.toIgnore || document.id(props.ignoreChildrenOf).getElements('input, select, textarea')).each(function (item) {
				if (element.checked) {
					fv.ignoreField(item);
					fv.resetField(item);
				} else {
					fv.enforceField(item);
				}
			});
			return true;
		}
	}],

	['validate-nospace', {
		errorMsg: function () {
			return Form.Validator.getMsg('noSpace');
		},
		test: function (element, props) {
			return !element.get('value').test(/\s/);
		}
	}],

	['validate-toggle-oncheck', {
		test: function (element, props) {
			var fv = element.getParent('form').retrieve('validator');
			if (!fv) { return true; }
			var eleArr = props.toToggle || document.id(props.toToggleChildrenOf).getElements('input, select, textarea');
			if (!element.checked) {
				eleArr.each(function (item) {
					fv.ignoreField(item);
					fv.resetField(item);
				});
			} else {
				eleArr.each(function (item) {
					fv.enforceField(item);
				});
			}
			return true;
		}
	}],

	['validate-reqchk-bynode', {
		errorMsg: function () {
			return Form.Validator.getMsg('reqChkByNode');
		},
		test: function (element, props) {
			return (document.id(props.nodeId).getElements(props.selector || 'input[type=checkbox], input[type=radio]')).some(function (item) {
				return item.checked;
			});
		}
	}],

	['validate-required-check', {
		errorMsg: function (element, props) {
			return props.useTitle ? element.get('title') : Form.Validator.getMsg('requiredChk');
		},
		test: function (element, props) {
			return !!element.checked;
		}
	}],

	['validate-reqchk-byname', {
		errorMsg: function (element, props) {
			return Form.Validator.getMsg('reqChkByName').substitute({ label: props.label || element.get('type') });
		},
		test: function (element, props) {
			var grpName = props.groupName || element.get('name');
			var oneCheckedItem = $$(document.getElementsByName(grpName)).some(function (item, index) {
				return item.checked;
			});
			var fv = element.getParent('form').retrieve('validator');
			if (oneCheckedItem && fv) fv.resetField(element);
			return oneCheckedItem;
		}
	}],

	['validate-match', {
		errorMsg: function (element, props) {
			return Form.Validator.getMsg('match').substitute({ matchName: props.matchName || document.id(props.matchInput).get('name') });
		},
		test: function (element, props) {
			var eleVal = element.get('value');
			var matchVal = document.id(props.matchInput) && document.id(props.matchInput).get('value');
			return eleVal && matchVal ? eleVal == matchVal : true;
		}
	}],

	['validate-after-date', {
		errorMsg: function (element, props) {
			return Form.Validator.getMsg('afterDate').substitute({
				label: props.afterLabel || (props.afterElement ? Form.Validator.getMsg('startDate') : Form.Validator.getMsg('currentDate'))
			});
		},
		test: function (element, props) {
			var start = document.id(props.afterElement) ? Date.parse(document.id(props.afterElement).get('value')) : new Date();
			var end = Date.parse(element.get('value'));
			return end && start ? end >= start : true;
		}
	}],

	['validate-before-date', {
		errorMsg: function (element, props) {
			return Form.Validator.getMsg('beforeDate').substitute({
				label: props.beforeLabel || (props.beforeElement ? Form.Validator.getMsg('endDate') : Form.Validator.getMsg('currentDate'))
			});
		},
		test: function (element, props) {
			var start = Date.parse(element.get('value'));
			var end = document.id(props.beforeElement) ? Date.parse(document.id(props.beforeElement).get('value')) : new Date();
			return end && start ? end >= start : true;
		}
	}],

	['validate-custom-required', {
		errorMsg: function () {
			return Form.Validator.getMsg('required');
		},
		test: function (element, props) {
			return element.get('value') != props.emptyValue;
		}
	}],

	['validate-same-month', {
		errorMsg: function (element, props) {
			var startMo = document.id(props.sameMonthAs) && document.id(props.sameMonthAs).get('value');
			var eleVal = element.get('value');
			if (eleVal != '') {
				return Form.Validator.getMsg(startMo ? 'sameMonth' : 'startMonth');
			}
		},
		test: function (element, props) {
			var d1 = Date.parse(element.get('value'));
			var d2 = Date.parse(document.id(props.sameMonthAs) && document.id(props.sameMonthAs).get('value'));
			return d1 && d2 ? d1.format('%B') == d2.format('%B') : true;
		}
	}],

	['validate-cc-num', {
		errorMsg: function (element) {
			var ccNum = element.get('value').replace(/[^0-9]/g, '');
			return Form.Validator.getMsg('creditcard').substitute({ length: ccNum.length });
		},
		test: function (element) {
			// required is a different test
			if (Form.Validator.getValidator('IsEmpty').test(element)) {
				return true;
			}

			// Clean number value
			var ccNum = element.get('value');
			ccNum = ccNum.replace(/[^0-9]/g, '');

			var valid_type = false;

			if (ccNum.test(/^4[0-9]{12}([0-9]{3})?$/)) {
				valid_type = 'Visa';
			} else if (ccNum.test(/^5[1-5]([0-9]{14})$/)) {
				valid_type = 'Master Card';
			} else if (ccNum.test(/^3[47][0-9]{13}$/)) {
				valid_type = 'American Express';
			} else if (ccNum.test(/^6011[0-9]{12}$/)) {
				valid_type = 'Discover';
			}

			if (valid_type) {
				var sum = 0,
						cur = 0;

				for (var i = ccNum.length - 1; i >= 0; --i) {
					cur = ccNum.charAt(i).toInt();
					if (cur == 0) { continue; }

					if ((ccNum.length - i) % 2 == 0) {
						cur += cur;
					}
					if (cur > 9) {
						cur = cur.toString().charAt(0).toInt() + cur.toString().charAt(1).toInt();
					}

					sum += cur;
				}
				if ((sum % 10) == 0) { return true; }
			}

			var chunks = '';
			while (ccNum != '') {
				chunks += ' ' + ccNum.substr(0, 4);
				ccNum = ccNum.substr(4);
			}

			element.getParent('form').retrieve('validator').ignoreField(element);
			element.set('value', chunks.clean());
			element.getParent('form').retrieve('validator').enforceField(element);
			return false;
		}
	}]

]);

	// #endregion Form.Validator.Extras

	Form.Validator.add('validate-currency-yuan', {

		errorMsg: function () {
			return Form.Validator.getMsg('currencyYuan');
		},

		test: function (element) {
			// [￥]1[##][,###]+[.##]
			// [￥]1###+[.##]
			// [￥]0.##
			// [￥].##
			return Form.Validator.getValidator('IsEmpty').test(element) || (/^￥?\-?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}\d*(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)$/).test(element.get('value'));
		}

	});

	// #region -- OverText --

	/**
	* name: OverText
	* 
	* description: Shows text over an input that disappears when the user clicks into it. The text remains hidden if the user adds a value.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Options
	*		- Core/Events
	*		- Core/Element.Event
	*		- Class.Binds
	*		- Class.Occlude
	*		- Element.Position
	*		- Element.Shortcuts
	* 
	* provides: [OverText]
	**/

	var OverText = this.OverText = new Class({

		Implements: [Options, Events, Class.Occlude],

		Binds: ['reposition', 'assert', 'focus', 'hide'],

		options: {/*
		textOverride: null,
		onFocus: function(){},
		onTextHide: function(textEl, inputEl){},
		onTextShow: function(textEl, inputEl){}, */
			element: 'label',
			labelClass: 'overTxtLabel',
			positionOptions: {
				position: 'upperLeft',
				edge: 'upperLeft',
				offset: {
					x: 4,
					y: 2
				}
			},
			poll: false,
			pollInterval: 250,
			wrap: false
		},

		property: 'OverText',

		initialize: function (element, options) {
			element = this.element = document.id(element);

			if (this.occlude()) { return this.occluded; }
			this.setOptions(options);

			this.attach(element);
			OverText.instances.push(this);

			if (this.options.poll) { this.poll(); }
		},

		toElement: function () {
			return this.element;
		},

		attach: function () {
			var element = this.element,
			options = this.options,
			value = options.textOverride || element.get('alt') || element.get('title');

			if (!value) { return this; }

			var text = this.text = new Element(options.element, {
				'class': options.labelClass,
				styles: {
					lineHeight: 'normal',
					position: 'absolute',
					cursor: 'text'
				},
				html: value,
				events: {
					click: this.hide.pass(options.element == 'label', this)
				}
			}).inject(element, 'after');

			if (options.element == 'label') {
				if (!element.get('id')) {
					element.set('id', 'input_' + String.uniqueID());
				}
				text.set('for', element.get('id'));
			}

			if (options.wrap) {
				this.textHolder = new Element('div.overTxtWrapper', {
					styles: {
						lineHeight: 'normal',
						position: 'relative'
					}
				}).grab(text).inject(element, 'before');
			}

			return this.enable();
		},

		destroy: function () {
			this.element.eliminate(this.property); // Class.Occlude storage
			this.disable();
			if (this.text) { this.text.destroy(); }
			if (this.textHolder) { this.textHolder.destroy(); }
			return this;
		},

		disable: function () {
			this.element.removeEvents({
				focus: this.focus,
				blur: this.assert,
				change: this.assert
			});
			window.removeEvent('resize', this.reposition);
			this.hide(true, true);
			return this;
		},

		enable: function () {
			this.element.addEvents({
				focus: this.focus,
				blur: this.assert,
				change: this.assert
			});
			window.addEvent('resize', this.reposition);
			this.reposition();
			return this;
		},

		wrap: function () {
			if (this.options.element == 'label') {
				if (!this.element.get('id')) {
					this.element.set('id', 'input_' + String.uniqueID());
				}
				this.text.set('for', this.element.get('id'));
			}
		},

		startPolling: function () {
			this.pollingPaused = false;
			return this.poll();
		},

		poll: function (stop) {
			//start immediately
			//pause on focus
			//resumeon blur
			if (this.poller && !stop) { return this; }
			if (stop) {
				clearInterval(this.poller);
			} else {
				this.poller = (function () {
					if (!this.pollingPaused) { this.assert(true); }
				}).periodical(this.options.pollInterval, this);
			}

			return this;
		},

		stopPolling: function () {
			this.pollingPaused = true;
			return this.poll(true);
		},

		focus: function () {
			if (this.text && (!this.text.isDisplayed() || this.element.get('disabled'))) {
				return this;
			}
			return this.hide();
		},

		hide: function (suppressFocus, force) {
			if (this.text && (this.text.isDisplayed() && (!this.element.get('disabled') || force))) {
				this.text.hide();
				this.fireEvent('textHide', [this.text, this.element]);
				this.pollingPaused = true;
				if (!suppressFocus) {
					try {
						this.element.fireEvent('focus');
						this.element.focus();
					} catch (e) { } //IE barfs if you call focus on hidden elements
				}
			}
			return this;
		},

		show: function () {
			if (this.text && !this.text.isDisplayed()) {
				this.text.show();
				this.reposition();
				this.fireEvent('textShow', [this.text, this.element]);
				this.pollingPaused = false;
			}
			return this;
		},

		test: function () {
			return !this.element.get('value');
		},

		assert: function (suppressFocus) {
			return this[this.test() ? 'show' : 'hide'](suppressFocus);
		},

		reposition: function () {
			this.assert(true);
			if (!this.element.isVisible()) {
				return this.stopPolling().hide();
			}
			if (this.text && this.test()) {
				this.text.position(Object.merge(this.options.positionOptions, {
					relativeTo: this.element
				}));
			}
			return this;
		}

	});

	OverText.instances = [];

	Object.append(OverText, {

		each: function (fn) {
			return OverText.instances.each(function (ot, i) {
				if (ot.element && ot.text) {
					fn.call(OverText, ot, i);
				}
			});
		},

		update: function () {

			return OverText.each(function (ot) {
				return ot.reposition();
			});

		},

		hideAll: function () {

			return OverText.each(function (ot) {
				return ot.hide(true, true);
			});

		},

		showAll: function () {
			return OverText.each(function (ot) {
				return ot.show();
			});
		}

	});

	// #endregion

});
